Android Https证书详解

背景

现在主流APP基本都在使用https做数据请求的通道了,相比较于Http,Https多了一个TLS的加密协议(传输层安全协议),具体Https的介绍在blog中已经写过,这里记录下在实际开发过程中,客户端要怎么配置。

TLS

TLS是基于 X.509 认证,他假定所有的数字证书都是由一个层次化的数字证书认证机构发出,即 CA。另外值得一提的是 TLS 是独立于 HTTP 的,任何应用层的协议都可以基于 TLS 建立安全的传输通道,如 SSH 协议。

CA

Https通信过程中需要交换服务器的公钥,但是怎么确保公钥就是服务器的公钥呢,就需要引入了一个第三方,也就是上面所说的 CA(Certificate Authority)。
CA 用自己的私钥签发数字证书,数字证书中包含A的公钥。然后 B 可以用 CA 的根证书中的公钥来解密 CA 签发的证书,从而拿到合法的公钥。那么又引入了一个问题,如何保证 CA 的公钥是合法的呢。答案就是现代主流的浏览器会内置 CA 的证书。我们可以在浏览器中看到Https网站的证书信息:

中间证书

当然,现在大多数CA不直接签署服务器证书,而是签署中间CA,然后用中间CA来签署服务器证书。这样根证书可以离线存储来确保安全,即使中间证书出了问题,可以用根证书重新签署中间证书。上图中第三级就是中间证书了。

Android配置Https

Android 使用的是 Java 的 API。那么 Https 使用的 Socket 必然都是通过SSLSocketFactory 创建的 SSLSocket,当然自己实现了 TLS 协议除外。目前Android使用的网络通信基本都是Okhttp了,OK默认就支持Https,当你不配置的时候,它默认是支持在Android内部默认安装的100多个证书,在Android设置中可以看到这些内置根证书(会自动更新)。

如果你的后端证书是购买的那么基本就是这些内置根证书中的一种了,你可以不需要任何改动,直接就可以从Http过渡到Https(直接修改BaseURL),但是如果后端使用的是自制证书,那么你就必须要配置了(如果不配置会报证书锚点找不到的错误)。

  • SSLSocketFactory

创建SSL的工厂类,默认是这样实现的:

1
2
3
4
5
6
7
8
9
 private synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, null, null);
return defaultSslSocketFactory = sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
  • TrustManager

上文说了,SSL 握手开始后,会校验服务器的证书,那么其实就是通过 X509ExtendedTrustManager 做校验的,更一般性的说是 X509TrustManager :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* The trust manager for X509 certificates to be used to perform authentication
* for secure sockets.
*/
public interface X509TrustManager extends TrustManager {

public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException;

public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException;

public X509Certificate[] getAcceptedIssuers();
}

那么最后校验服务器证书的过程会落到 checkServerTrusted 这个函数,如果校验没通过会抛出 CertificateException 。很多博客说,配置 SSL 差不多是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

}

public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}

public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}, null);
return sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError();
}
}

这个是信任所有证书的,包括自制证书,相当于客户端不会去检查证书的签名。这么做毫无安全性可言,一般不要这么做

SSL的配置

Android中SSL的配置,可以不配置(系统会默认信任Android内置证书),但是如果用系统默认的 SSL,那么就是假设一切 CA 都是可信的。可往往 CA 有时候也不可信,比如某家 CA 被黑客入侵什么的事屡见不鲜。虽然 Android 系统自身可以更新信任的 CA 列表,以防止一些 CA 的失效。那么为了更高的安全性,我们希望指定信任的锚点(我们信任的证书),可以类似采用如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Created by Ervin on 2017/3/14.
*/

public class CertificationFactory {

public static SSLContext getSLLContext(Context context){
SSLContext sslContext = null;
try {
//取得本地证书的流
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream cerInputStream = context.getAssets().open("root.crt");
Certificate ca = cf.generateCertificate(cerInputStream);

//创建Keystore包含我们的证书
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// 创建一个 TrustManager 仅把 Keystore 中的证书 作为信任的锚点
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
TrustManager[] trustManagers = tmf.getTrustManagers();

// 用 TrustManager 初始化一个 SSLContext
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return sslContext;
}
}

在Okhttp中开启配置:

1
2
3
4
5
6
7
8
9
10
11
httpClient = new OkHttpClient
.Builder()
.addInterceptor(new TokenValidInterceptor(this.context))
.addInterceptor(new HttpRespV3ConvertInterceptor())
.addInterceptor(loggingInterceptor)
//开启SSL配置
.sslSocketFactory(CertificationFactory.getSLLContext(this.context).getSocketFactory())
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build();

这样的话Okhttp会只信任“root.crt”以及被它签发的证书才会被信任。这里有个地方要注意:这里的root.crt在我的项目中代表了CA的根证书(第一级的证书,也许也是中间证书,过期时间比较久),还有一种证书是用这个root去签发的,在你购买证书后会给你去服务器配置的。因为在实际情况中,一般购买的证书(子证书)有效期都是2年左右,如果客户端信任这个子证书那么也就是2年后证书过期,你的APP就不能用了,因此我们考虑了使用根证书,(根证书默认会信任他以及他所签发的证书),这样子证书到期后,只要还用这家CA的证书签发一个子证书,APP还是会继续信任。这样比起信任100多个根证书,我只信任一个要安全得多。